1 | import fs from 'fs-extra'
|
2 | import path from 'path'
|
3 |
|
4 | import require_hacker from 'require-hacker'
|
5 | import serialize from '../tools/serialize-javascript'
|
6 |
|
7 | import { exists, clone, replace_all, starts_with, last } from '../helpers'
|
8 | import { alias_hook, uniform_path } from '../common'
|
9 |
|
10 |
|
11 | export default function write_assets(json, options, log)
|
12 | {
|
13 |
|
14 | options = clone(options)
|
15 |
|
16 | log.debug(`running write assets webpack plugin v${require('../../package.json').version} with options`, options)
|
17 |
|
18 |
|
19 | options.webpack_stats = json
|
20 |
|
21 | const development = options.development
|
22 |
|
23 | if (development)
|
24 | {
|
25 | log.debug(' (development mode is on)')
|
26 | }
|
27 |
|
28 |
|
29 | if (options.debug)
|
30 | {
|
31 |
|
32 | log.debug(`writing webpack stats to ${options.webpack_stats_path}`)
|
33 |
|
34 |
|
35 |
|
36 | fs.outputFileSync(options.webpack_stats_path, JSON.stringify(json, null, 2))
|
37 | }
|
38 |
|
39 |
|
40 | const output = options.output
|
41 |
|
42 |
|
43 | populate_assets(output, json, options, log)
|
44 |
|
45 |
|
46 | if (options.output_to_a_file)
|
47 | {
|
48 |
|
49 | const assets_info = development ? JSON.stringify(output, null, 2) : JSON.stringify(output)
|
50 |
|
51 |
|
52 | let rewrite = true
|
53 |
|
54 |
|
55 |
|
56 |
|
57 |
|
58 |
|
59 |
|
60 |
|
61 |
|
62 |
|
63 |
|
64 |
|
65 |
|
66 |
|
67 |
|
68 |
|
69 |
|
70 |
|
71 |
|
72 |
|
73 |
|
74 |
|
75 |
|
76 | if (rewrite)
|
77 | {
|
78 | log.debug(`writing webpack assets info to ${options.webpack_assets_path}`)
|
79 |
|
80 | fs.outputFileSync(options.webpack_assets_path, assets_info)
|
81 | }
|
82 | }
|
83 | else
|
84 | {
|
85 | log.debug(`serving webpack assets from memory`)
|
86 | }
|
87 |
|
88 |
|
89 |
|
90 | return output
|
91 | }
|
92 |
|
93 |
|
94 | function populate_assets(output, json, options, log)
|
95 | {
|
96 |
|
97 | Object.keys(json.assetsByChunkName).forEach(function(name)
|
98 | {
|
99 | log.debug(`getting javascript and styles for chunk "${name}"`)
|
100 |
|
101 |
|
102 |
|
103 | const javascript = get_assets(name, 'js')[0]
|
104 |
|
105 |
|
106 | if (javascript)
|
107 | {
|
108 | log.debug(` (got javascript)`)
|
109 | output.javascript[name] = javascript
|
110 | }
|
111 |
|
112 |
|
113 |
|
114 | const style = get_assets(name, 'css')[0]
|
115 |
|
116 |
|
117 | if (style)
|
118 | {
|
119 | log.debug(` (got style)`)
|
120 | output.styles[name] = style
|
121 | }
|
122 | })
|
123 |
|
124 |
|
125 | function get_assets(name, extension = 'js')
|
126 | {
|
127 | let chunk = json.assetsByChunkName[name]
|
128 |
|
129 |
|
130 | if (!(Array.isArray(chunk)))
|
131 | {
|
132 | chunk = [chunk]
|
133 | }
|
134 |
|
135 | return chunk
|
136 |
|
137 | .filter(name => path.extname(name) === `.${extension}`)
|
138 |
|
139 | .map(name => options.assets_base_url + name)
|
140 | }
|
141 |
|
142 |
|
143 | const default_filter = (module, regular_expression) => regular_expression.test(module.name)
|
144 |
|
145 | const default_asset_path = module => module.name
|
146 |
|
147 | const default_parser = module => module.source
|
148 |
|
149 |
|
150 | const parsed_assets = {}
|
151 |
|
152 |
|
153 | const global_paths_to_parsed_asset_paths = {}
|
154 |
|
155 |
|
156 |
|
157 | const define_webpack_public_path = 'var __webpack_public_path__ = ' + JSON.stringify(options.assets_base_url) + ';\n'
|
158 |
|
159 |
|
160 | for (let asset_type of Object.keys(options.assets))
|
161 | {
|
162 |
|
163 | const asset_type_settings = options.assets[asset_type]
|
164 |
|
165 |
|
166 | const filter = (asset_type_settings.filter || default_filter)
|
167 |
|
168 | const extract_asset_path = (asset_type_settings.path || default_asset_path)
|
169 |
|
170 | const parser = (asset_type_settings.parser || default_parser)
|
171 |
|
172 |
|
173 |
|
174 |
|
175 | if (!asset_type_settings.filter)
|
176 | {
|
177 | log.debug(`No filter specified for "${asset_type}" assets. Using a default one.`)
|
178 | }
|
179 |
|
180 |
|
181 | if (!asset_type_settings.path)
|
182 | {
|
183 | log.debug(`No path parser specified for "${asset_type}" assets. Using a default one.`)
|
184 | }
|
185 |
|
186 |
|
187 | if (!asset_type_settings.parser)
|
188 | {
|
189 | log.debug(`No parser specified for "${asset_type}" assets. Using a default one.`)
|
190 | }
|
191 |
|
192 | log.debug(`parsing assets of type "${asset_type}"`)
|
193 |
|
194 |
|
195 | const began_at = new Date().getTime()
|
196 |
|
197 |
|
198 | json.modules
|
199 |
|
200 | .filter(module =>
|
201 | {
|
202 |
|
203 | if (!filter(module, options.regular_expressions[asset_type], options, log))
|
204 | {
|
205 | return false
|
206 | }
|
207 |
|
208 |
|
209 | if (!module.source)
|
210 | {
|
211 | log.error(`Module "${module.name}" has no source. Maybe Webpack compilation of this module failed. Skipping this asset.`)
|
212 | return false
|
213 | }
|
214 |
|
215 |
|
216 | return true
|
217 | })
|
218 | .reduce((set, module) =>
|
219 | {
|
220 |
|
221 | const asset_path = extract_asset_path(module, options, log)
|
222 |
|
223 |
|
224 | const parsed_asset = parser(module, options, log)
|
225 |
|
226 | log.trace(`Adding asset "${asset_path}", module id ${module.id} (in webpack-stats.json)`)
|
227 |
|
228 |
|
229 | if (exists(set[asset_path]))
|
230 | {
|
231 | log.error('-----------------------------------------------------------------')
|
232 | log.error(`Asset with path "${asset_path}" was overwritten because of path collision.`)
|
233 | log.error(`Use the "filter" function of this asset type to narrow the results.`)
|
234 | log.error(`Previous asset with this path:`)
|
235 | log.error(set[asset_path])
|
236 | log.error(`New asset with this path:`)
|
237 | log.error(parsed_asset)
|
238 | log.error('-----------------------------------------------------------------')
|
239 | }
|
240 |
|
241 | // add this asset to the list
|
242 | //
|
243 | // also resolve "ReferenceError: __webpack_public_path__ is not defined".
|
244 | // because it may be a url-loaded resource (e.g. a font inside a style).
|
245 | set[asset_path] = define_webpack_public_path + require_hacker.to_javascript_module_source(parsed_asset)
|
246 |
|
247 | // add path mapping
|
248 | global_paths_to_parsed_asset_paths[path.resolve(options.project_path, asset_path)] = asset_path
|
249 |
|
250 | // continue
|
251 | return set
|
252 | },
|
253 | parsed_assets)
|
254 |
|
255 | // timer stop
|
256 | log.debug(` time taken: ${new Date().getTime() - began_at} ms`)
|
257 | }
|
258 |
|
259 | // register a special require() hook for requiring() raw webpack modules
|
260 | const require_hook = require_hacker.global_hook('webpack-module', (required_path, module) =>
|
261 | {
|
262 | log.debug(`require()ing "${required_path}"`)
|
263 |
|
264 | // if Webpack aliases are supplied
|
265 | if (options.alias)
|
266 | {
|
267 | // possibly alias the path
|
268 | const aliased_global_path = alias_hook(required_path, module, options.project_path, options.alias, log)
|
269 |
|
270 | // if an alias is found
|
271 | if (aliased_global_path)
|
272 | {
|
273 | return require_hacker.to_javascript_module_source(safe_require(aliased_global_path, log))
|
274 | }
|
275 | }
|
276 |
|
277 | // find an asset with this path
|
278 | //
|
279 | // the require()d path will be global path in case of the for..of require() loop
|
280 | // for the assets (the code a couple of screens below).
|
281 | //
|
282 | // (it can be anything in other cases (e.g. nested require() calls from the assets))
|
283 | //
|
284 | if (exists(global_paths_to_parsed_asset_paths[required_path]))
|
285 | {
|
286 | log.debug(` found in parsed assets`)
|
287 | return parsed_assets[global_paths_to_parsed_asset_paths[required_path]]
|
288 | }
|
289 |
|
290 | log.debug(` not found in parsed assets, searching in webpack stats`)
|
291 |
|
292 | // find a webpack module which has a reason with this path
|
293 |
|
294 | const candidates = []
|
295 |
|
296 | for (let module of json.modules)
|
297 | {
|
298 | for (let reason of module.reasons)
|
299 | {
|
300 | if (reason.userRequest === required_path)
|
301 | {
|
302 | candidates.push(module)
|
303 | break
|
304 | }
|
305 | }
|
306 | }
|
307 |
|
308 | // guard against ambiguity
|
309 |
|
310 | if (candidates.length === 1)
|
311 | {
|
312 | log.debug(` found in webpack stats, module id ${candidates[0].id}`)
|
313 |
|
314 | // also resolve "ReferenceError: __webpack_public_path__ is not defined".
|
315 | // because it may be a url-loaded resource (e.g. a font inside a style).
|
316 | return define_webpack_public_path + candidates[0].source
|
317 | }
|
318 |
|
319 | // if there are more than one candidate for this require()d path,
|
320 | // then try to guess which one is the one require()d
|
321 |
|
322 | if (candidates.length > 1)
|
323 | {
|
324 | log.debug(` More than a single candidate module was found in webpack stats for require()d path "${required_path}"`)
|
325 |
|
326 | for (let candidate of candidates)
|
327 | {
|
328 | log.debug(' ', candidate)
|
329 | }
|
330 |
|
331 | // (loaders matter so the program can't simply throw them away from the required path)
|
332 | //
|
333 | // // tries to normalize a cryptic Webpack loader path
|
334 | // // into a regular relative file path
|
335 | // // https://webpack.github.io/docs/loaders.html
|
336 | // let filesystem_required_path = last(required_path
|
337 | // .replace(/^!!/, '')
|
338 | // .replace(/^!/, '')
|
339 | // .replace(/^-!/, '')
|
340 | // .split('!'))
|
341 |
|
342 | const fail = () =>
|
343 | {
|
344 | throw new Error(`More than a single candidate module was found in webpack stats for require()d path "${required_path}". Enable "debug: true" flag in webpack-isomorphic-tools configuration for more info.`)
|
345 | }
|
346 |
|
347 | // https://webpack.github.io/docs/loaders.html
|
348 | const is_webpack_loader_path = required_path.indexOf('!') >= 0
|
349 |
|
350 | // if it's a Webpack loader-powered path, the code gives up
|
351 | if (is_webpack_loader_path)
|
352 | {
|
353 | fail()
|
354 | }
|
355 |
|
356 | // from here on it's either a filesystem path or an npm module path
|
357 |
|
358 | const is_a_global_path = path => starts_with(path, '/') || path.indexOf(':') > 0
|
359 | const is_a_relative_path = path => starts_with(path, './') || starts_with(path, '../')
|
360 |
|
361 | const is_relative_path = is_a_relative_path(required_path)
|
362 | const is_global_path = is_a_global_path(required_path)
|
363 | const is_npm_module_path = !is_relative_path && !is_global_path
|
364 |
|
365 | // if it's a global path it can be resolved right away
|
366 | if (is_global_path)
|
367 | {
|
368 | return require_hacker.to_javascript_module_source(safe_require(required_path, log))
|
369 | }
|
370 |
|
371 | // from here on it's either a relative filesystem path or an npm module path,
|
372 | // so it can be resolved against the require()ing file path (if it can be recovered).
|
373 |
|
374 | // `module.filename` here can be anything, not just a filesystem absolute path,
|
375 | // since some advanced require() hook trickery is involved.
|
376 | // therefore it will be parsed.
|
377 | //
|
378 | let requiring_file_path = module.filename.replace(/\.webpack-module$/, '')
|
379 |
|
380 | // if it's a webpack loader-powered path, then extract the filesystem path from it
|
381 | if (requiring_file_path.indexOf('!') >= 0)
|
382 | {
|
383 | requiring_file_path = requiring_file_path.substring(requiring_file_path.lastIndexOf('!') + 1)
|
384 | }
|
385 |
|
386 | // make relative path global
|
387 | if (is_a_relative_path(requiring_file_path))
|
388 | {
|
389 | requiring_file_path = path.resolve(options.project_path, requiring_file_path)
|
390 | }
|
391 |
|
392 | // if `requiring_file_path` is a filesystem path (not an npm module path),
|
393 | // then the require()d path can possibly be resolved
|
394 | if (is_a_global_path(requiring_file_path))
|
395 | {
|
396 | log.debug(` The module is being require()d from "${requiring_file_path}", so resolving the path against this file`)
|
397 |
|
398 | // if it's a relative path, can try to resolve it
|
399 | if (is_relative_path)
|
400 | {
|
401 | return require_hacker.to_javascript_module_source(safe_require(path.resolve(requiring_file_path, '..', required_path), log))
|
402 | }
|
403 |
|
404 | // if it's an npm module path (e.g. 'babel-runtime/core-js/object/assign'),
|
405 | // can try to require() it from the requiring asset path
|
406 | if (is_npm_module_path && is_a_global_path(module.filename))
|
407 | {
|
408 | return require_hacker.to_javascript_module_source(safe_require(require_hacker.resolve(required_path, module), log))
|
409 | }
|
410 | }
|
411 |
|
412 | // if it's still here then it means it's either a
|
413 | fail()
|
414 | }
|
415 | })
|
416 |
|
417 | log.debug(`compiling assets`)
|
418 |
|
419 | // timer start
|
420 | const began_at = new Date().getTime()
|
421 |
|
422 | // evaluate parsed assets source code
|
423 | for (let asset_path of Object.keys(parsed_assets))
|
424 | {
|
425 | // set asset value
|
426 | log.debug(`compiling asset "${asset_path}"`)
|
427 | output.assets[asset_path] = safe_require(path.resolve(options.project_path, asset_path), log)
|
428 |
|
429 | // inside that require() call above
|
430 | // all the assets are resolved relative to this `module`,
|
431 | // which is irrelevant because they are all absolute filesystem paths.
|
432 | //
|
433 | // if in some of those assets a nested require() call is present
|
434 | // then it will be resolved relative to that asset folder.
|
435 | }
|
436 |
|
437 | // unmount the previously installed require() hook
|
438 | require_hook.unmount()
|
439 |
|
440 | // timer stop
|
441 | log.debug(` time taken: ${new Date().getTime() - began_at} ms`)
|
442 | }
|
443 |
|
444 | function safe_require(path, log)
|
445 | {
|
446 | try
|
447 | {
|
448 | return require(path)
|
449 | }
|
450 | catch (error)
|
451 | {
|
452 | log.error(error)
|
453 | return undefined
|
454 | }
|
455 | } |
\ | No newline at end of file |